#!/usr/bin/env bash
#
# passrofi is a password-store UI using rofi
# Copyright (C) 2020 Distopico <distopico@riseup.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Description:
# --------------
# Simple password-store aka 'pass' UI for rofi or dmenu
#
# Usage:
# ---------------
# * rofi mode: rofi -modi "pass:passrofi" -show
# * rofi dmenu: /path/to/script/passrofi
#
shopt -s nullglob globstar

# Options
COPY_PASS="copy pass"
COPY_OTP="copy otp"
COPY_FIELD="copy field"
SHOW_FIELD="show field"
COPY_ACTION="copy"
SHOW_ACTION="copy"
QUIT_NAME="quit ›"
RETURN_NAME="‹ return"
OPTIONS=( "$COPY_PASS|*" "$COPY_OTP|•" "$COPY_FIELD|»" "$SHOW_FIELD|›" )
ACTIONS=( "$COPY_ACTION|‣" "$SHOW_ACTION|▹" )

# Setup
BASE_PATH=$(dirname "$0")
PASS_CMD=${PASSWORD_STORE_CMD:-"$BASE_PATH/passp"}
PASS_CLIP_TIME=${PASSWORD_STORE_CLIP_TIME:-45}
PASS_ROOT_DIR=${PASSWORD_STORE_DIR-~/.password-store}
PASS_X_SELECTION=${PASSWORD_STORE_X_SELECTION:-clipboard}
PASS_ROFI_FIELDS_SEPARATOR=${PASSWORD_STORE_ROFI_FIELDS_SEPARATOR:-":"}
NOTIFY_CMD="notify-send"
PROMPT="pass"
BASE64="base64"

# Check commands
has_notify=0
rofi_mode=0
if [[ -x "$(command -v $NOTIFY_CMD)" ]]; then
    has_notify=1
fi
if ! [[ -x "$(command -v pgrep)" ]]; then
    # Set pgrep fallback
    function pgrep() {
        ps axf | grep $1 | grep -v grep | awk '{print $1}'
    }
fi
if ! [[ -z "$(pgrep "rofi")" ]]; then
    rofi_mode=1
fi


rofi_dmenu () {
    rofi -dmenu -p $PROMPT "$@"
}

# main menu
main_menu () {
    for opt in "${OPTIONS[@]}"
    do
        IFS='|' read -ra val <<< "$opt"
        printf "%s\n" "${val[0]}"
    done
}

# Clip with the available command in X or Wayland
clip() {
    if [[ -n $WAYLAND_DISPLAY ]]; then
		local copy_cmd=( wl-copy )
		local paste_cmd=( wl-paste -n )
		if [[ $PASS_X_SELECTION == primary ]]; then
			copy_cmd+=( --primary )
			paste_cmd+=( --primary )
		fi
		local display_name="$WAYLAND_DISPLAY"
	elif [[ -n $DISPLAY ]]; then
		local copy_cmd=( xclip -selection "$PASS_X_SELECTION" )
		local paste_cmd=( xclip -o -selection "$PASS_X_SELECTION" )
		local display_name="$DISPLAY"
	else
		die "Error: No X11 or Wayland display detected"
	fi
	local sleep_argv0="password store sleep on display $display_name"

	# This base64 is because bash cannot store binary data in a shell variable
	pkill -f "^$sleep_argv0" 2>/dev/null && sleep 0.5
	local before="$("${paste_cmd[@]}" 2>/dev/null | $BASE64)"
	echo -n "$1" | "${copy_cmd[@]}" >/dev/null 2>&1
	(
		( exec -a "$sleep_argv0" bash <<<"trap 'kill %1' TERM; sleep '$PASS_CLIP_TIME' & wait" )
		local now="$("${paste_cmd[@]}" | $BASE64)"
		[[ $now != $(echo -n "$1" | $BASE64) ]] && before="$now"

		echo "$before" | $BASE64 -d | "${copy_cmd[@]}"
	) >/dev/null 2>&1 & disown
}

# Get identifier type
get_option_type () {
    local options=("${OPTIONS[@]}", "${ACTIONS[@]}")
    value=""

    for opt in "${options[@]}"
    do
        IFS='|' read -ra val <<< "$opt"
        if [[ "$1" == "${val[0]}" ]] || [[ "$1" == "${val[1]}" ]]; then
            if [[ "$2" == "identifie" ]]; then
                value=${val[1]}
            else
                value=${val[0]}
            fi
            break
        fi
    done
    echo "$value"
}

# Get passwords list
get_pass_list () {
    password_files=( "$PASS_ROOT_DIR"/**/*.gpg )
    password_files=( "${password_files[@]#"$PASS_ROOT_DIR"/}" )
    password_files=( "${password_files[@]%.gpg}" )
    identifier=$(get_option_type "$@" "identifie")

    printf "$RETURN_NAME\n"
    printf "$identifier| %s\n" "${password_files[@]}"
}

# Call copy command
copy_pass () {
    IFS='|' read -ra val <<< "$@"
    action=$(get_option_type "${val[0]}")
    password="$(echo -e "${val[1]}" | sed -e 's/^[[:space:]]*//')"
    should_notify=0
    response=1

    [[ -n $password ]] || exit

    case "${action}" in
        "$COPY_PASS")
            $PASS_CMD -c "$password" >/dev/null 2>&1
            should_notify=1
            response=$?
            ;;
        "$COPY_OTP")
            $PASS_CMD otp -c "$password" >/dev/null 2>&1
            should_notify=1
            response=$?
            ;;
        "$COPY_FIELD")
            pass_field=$($PASS_CMD show "$password")
            response=$?
            pass_key_value=$(printf '%s\n' "${pass_field}" | tail -n+2)
            return_opt="$RETURN_NAME"
            empty_msg="not have values"

            # Check if password not have additional fields
            if [[ -z "$pass_key_value" ]]; then
                if [[ $rofi_mode -eq 1 ]]; then
                    echo -en "\0message\x1f<b>$password</b>: $empty_msg\n"
                    printf "%s\n" "$return_opt"
                else
                    printf "%s\n" "$password: $empty_msg | $RETURN_NAME"
                fi
                exit 0
            fi

            # Return opts
            printf "%s\n" "$return_opt"

            # Show pass fields
            local line=0
            while read -r LINE; do
			    line_key="${LINE%%: *}"
			    line_val="${LINE#* }"
                content="$line_key: $line_val"

                if [[ $line_key =~ "otpauth://" ]]; then
                    # exclude OTP value/secret
                    continue
                fi

                ((line++))
                if [[ $line_key = $line_val ]]; then
                    content="$line_val"
                fi
			    printf "‣| $line) %s [$password]\n" "$content"
		    done < <(printf "%s\n" "${pass_key_value}")
            ;;
        "$COPY_ACTION")
            local data=$(echo "$password" | sed 's/\([[:digit:]]\+\))[[:space:]]\+\(.*\)[[:space:]]\+\[\([^]]*\)\]/\1,\2,\3/')
            IFS="," read -ra field_data <<< "$data"
            IFS="$PASS_ROFI_FIELDS_SEPARATOR" read -ra field_values <<< "${field_data[1]}"
            local line=$(("${field_data[0]}" + 1))
            local field_len=${#field_values[@]}
            password="${field_data[2]}"

            # Field name or line to show on notification
            field=" (<b>${field_values[0]}</b>)"
            if [[ $field_len -lt 2 ]]; then
                field=" (<b>line: $line</b>)"
            fi

            # Copy non-password entry without field type
            # e.g. "email: myname@email.com" -> "myname@email.com"
            if [[ $line -gt 1 ]]; then
                # trim leading/trailing spaces
                local copy_value=$(echo "${field_values[1]}" | sed 's,^ *,,; s, *$,,')
                clip echo "$copy_value"
            fi
            should_notify=1
            response=$?
            ;;
        *)
            response=1
            ;;
    esac

    if [[ $response -eq 1 ]]; then
        if [[ $rofi_mode -eq 1 ]]; then
            echo -en "\0message\x1f<b>$action</b>: error copying\n"
            printf "$QUIT_NAME\n"
        else
            printf "$action: error copying | $QUIT_NAME\n"
        fi
    elif [[ $has_notify -eq 1 ]] && [[ $should_notify -eq 1 ]]; then
		$NOTIFY_CMD "pass" "Copied <b>$password</b>$field to clipboard. Will clear in $PASS_CLIP_TIME seconds." >/dev/null 2>&1
	fi
}

call_action () {
    if [[ $rofi_mode -eq 1 ]]; then
        if ! [[ -z $(get_option_type "$@") ]]; then
            get_pass_list "$@"
        else
            copy_pass "$@"
        fi
    else
        call_dmenu "$@"
    fi
}

call_dmenu () {
    action=$(main_menu | rofi_dmenu "$@")
    [[ -n $action ]] || exit

    password=$(get_pass_list "$action" | rofi_dmenu "$@")
    [[ -n $password ]] || exit

    result=$(copy_pass "$password")
    if [[ "$result" ]]; then
        echo "$result" | rofi_dmenu "$@"
    fi
}

call_main () {
    # Show as rofi mode or dmenu
    if [[ $rofi_mode -eq 1 ]]; then
        echo -en "\0message\x1f\n" # reset message
        echo -en "\x00prompt\x1f$PROMPT\n"
        main_menu
    else
        call_dmenu
    fi
}

# Inputs
if [[ x"$@" = x"$QUIT_NAME" ]]; then
    exit 0
elif [[ x"$@" = x"$RETURN_NAME" ]]; then
    call_main
elif [[ "$@" ]]; then
    call_action "$@"
else
    call_main
fi
